---
layout: layouts/page.njk
title: Command
description: Fast, composable command menu for quick navigation and actions.
toc:
  - label: Usage
    id: usage
    children:
      - label: HTML + JavaScript
        id: usage-html-js
        children:
          - label: "Step 1: Include the JavaScript files"
            id: usage-html-js-1
          - label: "Step 2: Add your command HTML"
            id: usage-html-js-2
          - label: HTML structure
            id: usage-html-js-3
          - label: JavaScript events
            id: usage-html-js-4
      - label: Jinja and Nunjucks
        id: usage-macro
  - label: Examples
    id: examples
    children:
      - label: Dialog
        id: example-dialog
---

{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "command.njk" import command, command_dialog %}

{% macro command_items(suggestions_id="suggestions", settings_id="settings") %}
<div role="group" aria-labelledby="{{ suggestions_id }}">
  <span role="heading" id="{{ suggestions_id }}">Suggestions</span>
  <div role="menuitem" onclick="console.log('Open calendar')">
    {% lucide "calendar" %}
    <span>Calendar</span>
  </div>
  <div role="menuitem" onclick="console.log('Search emoji')">
    {% lucide "smile" %}
    <span>Search Emoji</span>
  </div>
  <div role="menuitem" aria-disabled="true">
    {% lucide "calculator" %}
    <span>Calculator</span>
  </div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="{{ settings_id }}">
  <span role="heading" id="{{ settings_id }}">Settings</span>
  <div role="menuitem" onclick="console.log('Open profile')" data-filter="Profile">
    {% lucide "user" %}
    <span>Profile</span>
    <kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘P</kbd>
  </div>
  <div role="menuitem" onclick="console.log('Open billing')" data-filter="Billing">
    {% lucide "credit-card" %}
    <span>Billing</span>
    <kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘B</kbd>
  </div>
  <div role="menuitem" onclick="console.log('Open settings')" data-filter="Settings">
    {% lucide "settings" %}
    <span>Settings</span>
    <kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘S</kbd>
  </div>
</div>
{% endmacro %}

{% set code_standalone %}
{% call command(
  id="demo-command-standalone",
  main_attrs={ "class": "rounded-lg border shadow-md" }
) %}
{{ command_items("suggestions", "settings") }}
{% endcall %}
{% endset %}

{{ code_preview("command-standalone", code_standalone | prettyHtml, class=" w-full max-w-[450px]") }}

<h2 id="usage"><a href="#usage">Usage</a></h2>

<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>

<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>

<section class="prose">
  <p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>

{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/command.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}

<div class="flex flex-wrap gap-2 my-6">
  <a class="badge-outline" href="/installation/#install-js">
    Components with JavaScript
    {% lucide "arrow-right" %}
  </a>
  <a class="badge-outline" href="/installation/#install-cli">
    Use the CLI
    {% lucide "arrow-right" %}
  </a>
  <a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/command.js" target="_blank">
    command.js
    {% lucide "arrow-right" %}
  </a>
</div>

<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your command HTML</a></h4>

<section class="prose">
  <p>You can use the command menu standalone:</p>
</section>

{{ code_block(code_standalone | prettyHtml, "html") }}

<section class="prose">
  <p>Wrap the command menu in a <code>&lt;dialog class="command-dialog"&gt;</code> to display in a dialog:</p>
</section>

{% set code_dialog %}
<dialog class="command-dialog" aria-label="Command menu">
  <div class="command">
    <!-- Regular command content: header, menu, etc. -->
  </div>
</dialog>
{% endset %}

{{ code_block(code_dialog | prettyHtml, "html") }}

<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>

<section class="prose">
  <dl>
    <dt><code class="highlight language-html">&lt;dialog class="command-dialog"&gt;</code> <span class="badge-secondary">Optional</span></dt>
    <dd>For dialog variant, wraps a <code>&lt;div class="command"&gt;</code> to display as a modal. Uses native <code>&lt;dialog&gt;</code> behavior and requires <code>aria-label</code> or <code>aria-labelledby</code> for accessibility.
      <dl>
        <dt><code class="highlight language-html">&lt;div class="command"&gt;</code></dt>
        <dd>Wraps around the entire command component. Can be used standalone or inside a dialog.
          <dl>
            <dt><code class="highlight language-html">&lt;header&gt;</code></dt>
            <dd>Contains the search input to filter command options.
              <dl>
                <dt><code class="highlight language-html">&lt;input type="text"&gt;</code></dt>
                <dd>The search input. Should have:
                  <ul>
                    <li><code>role="combobox"</code>: indicates the input controls a menu with filtering</li>
                    <li><code>aria-expanded="true"</code>: the menu is always expanded</li>
                    <li><code>aria-controls="{ MENU_ID }"</code>: points to the menu's id</li>
                  </ul>
                </dd>
              </dl>
            </dd>
            <dt><code class="highlight language-html">&lt;div role="menu"&gt;</code></dt>
            <dd>The menu containing the command items. Should have <code>id="{ MENU_ID }"</code> refered to by the input's <code>aria-controls</code> attribute.
              <dl>
                <dt><code class="highlight language-html">&lt;div role="menuitem"&gt;</code></dt>
                <dd>Command item that triggers an action when clicked. In a command-dialog, clicking a menuitem automatically closes the dialog unless the <code>data-keep-command-open</code> attribute is present. Optional attributes:
                  <ul>
                    <li><code>data-filter</code>: custom text used for filtering (if different from displayed content, e.g., <code>data-filter="calendar event"</code>)</li>
                    <li><code>data-keywords</code>: additional keywords for enhanced filtering (e.g., <code>data-keywords="calendar event date"</code>)</li>
                    <li><code>data-force</code>: forces the item to always display, regardless of filter (e.g., <code>data-force</code>)</li>
                    <li><code>aria-disabled="true"</code>: marks item as disabled (excluded from keyboard navigation and filtering)</li>
                  </ul>
                </dd>
                <dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
                <dd>Separator between groups/items. Hidden when filtering is active.</dd>
                <dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
                <dd>Group of items, can have <code>aria-labelledby</code> attribute to link to a heading.</dd>
                <dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
                <dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
              </dl>
            </dd>
          </dl>
        </dd>
      </dl>
    </dd>
  </dl>
</section>

<h4 id="usage-html-js-4"><a href="#usage-html-js-4">JavaScript events</a></h4>

<section class="prose">
  <dl>
    <dt><code>basecoat:initialized</code></dt>
    <dd>Once the component is fully initialized, it dispatches a custom (non-bubbling) <code>basecoat:initialized</code> event on itself.</dd>
  </dl>
</section>

<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>

<div class="prose">
  <p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">command()</code> or <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">command_dialog()</code> Nunjucks or Jinja macros for this component.</p>
</div>

<div class="flex flex-wrap gap-2 my-6">
  <a class="badge-outline" href="/installation/#install-macros" target="_blank">
    Use Nunjucks or Jinja macros
    {% lucide "arrow-right" %}
  </a>
  <a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/command.html.jinja" target="_blank">
    Jinja macro
    {% lucide "arrow-right" %}
  </a>
  <a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/command.njk" target="_blank">
    Nunjucks macro
    {% lucide "arrow-right" %}
  </a>
</div>

{% set raw_code %}{% raw %}{% call command() %}
  <div role="menuitem">
    <svg>...</svg>
    <span>Calendar</span>
  </div>
  <div role="menuitem" data-keywords="config preferences">
    <svg>...</svg>
    <span>Settings</span>
  </div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}

<h2 id="examples"><a href="#examples">Examples</a></h2>

<h3 id="example-dialog"><a href="#example-dialog">Dialog</a></h3>

<section class="prose">
  <p>You can use some simple JavaScript to open the command menu with a keyboard shortcut (in this case <kbd class="kbd">⌘J</kbd>).</p>
</section>

{% set code_dialog %}
<button
  type="button"
  class="btn-outline"
  onclick="document.getElementById('demo-command-dialog').showModal()"
>
  Open command menu
  <kbd class="kbd">⌘J</kbd>
</button>

{% call command_dialog(id="demo-command-dialog") %}
{{ command_items("cmd-suggestions", "cmd-settings") }}
{% endcall %}

<script>
  document.addEventListener('keydown', (e) => {
    if ((e.metaKey || e.ctrlKey) && e.key === 'j') {
      e.preventDefault();
      const dialog = document.getElementById('demo-command-dialog');
      if (dialog.open) {
        dialog.close();
      } else {
        dialog.showModal();
        dialog.querySelector('header input')?.focus();
      }
    }
  });
</script>
{% endset %}

{{ code_preview("command-dialog", code_dialog | prettyHtml) }}
